/**
* file name  : it/ciano/jenoware/Genotype.java
* authors    : Luciano Xumerle
* created    : dom 05 set 2004 10:40:51 CEST
*
* Copyright (c) 2004-2005 Luciano Xumerle. All rights reserved.
*
* This file is part of gephinfo.
*
* gephinfo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*
*/

package it.ciano.jenoware;

import java.util.HashSet;
import java.util.Arrays;
import java.util.Iterator;


/**
 * The class Genotype manages a list of PairAlleles ojects.
 * Usually each subject has a single genotype for each marker, but in probabilistically
 * reconstructded haplotype or genotype this is not true.
 * Each subject has a list of possible genotypes with a given probability.
 * The class usually works considering a single possible genotype for each marker.
 * The user must manage manually multiple Genotipe through the methods addPairAllele and
 * so on. Read the methods documentation for more info.
 *
 * @author Luciano Xumerle
 * @version 0.0.1
 */
public class Genotype implements Comparable {
    private String _name = null;
    private String _date = null;

    // the class store the list of objects here.
    // I need an array to store probabilistic genotype in which there are
    // several values with a probability
    PairAllele[] _genotypes;

    // used when this is a binned Genotype
    private boolean _isBinned;
    private Genotype _toBin;


    /**
     * Return max value allowed for distance between the sequenced genotype and the binned genotype.
     */
    public final static float maxDistanceFromBinned = ( float ) 1.2;

    /**
     * Class constructor for a not valid Genotype.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name the marker name.
     */
    public Genotype ( String name ) {
        this ( name, new Allele(), new Allele(), "" );
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name    the marker name.
     * @param allele1 the first allele.
     * @param allele2 the second allele.
     */
    public Genotype ( String name, String allele1, String allele2 ) {
        this ( name, new Allele ( allele1 ), new Allele ( allele2 ), "" );
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name    the marker name.
     * @param allele1 the first allele.
     * @param allele2 the second allele.
     */
    public Genotype ( String name, Allele allele1, Allele allele2 ) {
        this ( name, allele1, allele2, "", 1, false );
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name    the marker name.
     * @param allele1 the first int allele.
     * @param allele2 the second int allele.
     */
    public Genotype ( String name, int allele1, int allele2 ) {
        this ( name, new Allele ( allele1 ), new Allele ( allele2 ), "" );
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name    the marker name.
     * @param allele1 the first allele.
     * @param allele2 the second allele.
     * @param date the date.
     */
    public Genotype ( String name, String allele1, String allele2, String date ) {
        this ( name, new Allele ( allele1 ), new Allele ( allele2 ), date );
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name the marker name.
     * @param allele1 the first allele.
     * @param allele2 the second allele.
     * @param date the date.
     */
    public Genotype ( String name, Allele allele1, Allele allele2, String date ) {
        this ( name, allele1, allele2, date, 1, false );
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name the marker name.
     * @param allele1 the first allele.
     * @param allele2 the second allele.
     * @param date the date.
     * @param freq the frequency (or probability for this pair)
     * @param useInveseOrder the order of allele is not lexicographical.
     */
    public Genotype ( String name, Allele allele1, Allele allele2, String date, double freq, boolean useInveseOrder ) {
        _genotypes=new PairAllele[] { new PairAllele ( allele1, allele2, freq, useInveseOrder ) };
        if ( date == null )
            _date = "";
        else
            _date = date.trim();
        _name = name;
        _toBin = null;
        _isBinned = false;
    }

    /**
     * Class Constructor.
     * Creates a Genotype with a single PairAllele.
     *
     * @param name the marker name.
     * @param position the position in the ordered pair array (0 is null).
     * @param orderedPair the list of the ordered PairAllele.
     * @param date the date.
     */
    public Genotype ( String name, int position, PairAllele[] orderedPair, String date ) {
        this ( name, orderedPair[ position ].getAllele ( 1 ),
               orderedPair[ position ].getAllele ( 2 ), date,
               orderedPair[ position ].getFrequency(),
               orderedPair[ position ].isMantainAllelesOrder() );
    }

    /**
     * Returns the size of the PairAllele list.
     *
     * @return the size of the PairAllele list.
     */
    public int size() {
        return _genotypes.length;
    }

    /**
     * Adds a PairAllele to the Genotype.
     * Each PairAllele should have a frequency to be useful.
     *
     * @param p the PairAllele object.
     */
    public void addPairAllele ( PairAllele p ) {
        if ( p==null || !p.isValid() )
            return;
        PairAllele[] tt=new PairAllele[_genotypes.length+1];
        tt[_genotypes.length]=p;
        for ( int i=0; i<_genotypes.length; i++ )
            tt[i]=_genotypes[i];
        _genotypes=tt;
    }

    /**
     * Returns first (1) or the second (2) Allele of the Genotype.
     * i=[1,2] because human is diploid.
     *
     * @param i the allele position.
     * @param pairAlleleInPosition The position of the PairAllele in the PairAllele list.
     * @return the required allele.
     */
    public Allele getAllele ( int i, int pairAlleleInPosition ) {
        if ( pairAlleleInPosition>-1 && pairAlleleInPosition<size() )
            return _genotypes[pairAlleleInPosition].getAllele ( i );
        return _genotypes[0].getAllele ( i );
    }

    /**
     * Returns first (1) or the second (2) Allele of the Genotype.
     * i=[1,2] because human is diploid.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param i the allele position.
     * @return the required allele.
     */
    public Allele getAllele ( int i ) {
        return getAllele ( i, 0 );
    }

    /**
     * Returns the first allele.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the first allele.
     */
    public String getAll1() {
        return _genotypes[0].getAllele ( 1 ).toString();
    }

    /**
     * Returns the second allele.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the second allele.
     */
    public String getAll2() {
        return _genotypes[0].getAllele ( 2 ).toString();
    }

    /**
     * The method check if a genotype is valid (alleles != "").
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return true or false.
     */
    public boolean isValid() {
        return _genotypes[0].isValid();
    }

    /**
     * Returns true if the genotype is numeric.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return true or false.
     */
    public boolean isNumeric() {
        return _genotypes[0].isNumeric();
    }

    /**
     * Returns true if the Genonotype is Homozigote (allele 1 == allele 2).
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return true or false.
     */
    public boolean isHomozigote() {
        return _genotypes[0].isHomozigote();
    }

    /**
     * Returns the frequency value for the Genotype (-1 if non set).
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the frequency value for the Genotype (-1 if non set).
     */
    public double getFrequency() {
        return  _genotypes[0].getFrequency();
    }

    /**
     * Returns the frequency value for the Genotype
     * with position <i>i</i> in the PairAllele list (-1 if non set).
     *
     * @param i the position in the PairAllele list.
     * @return the frequency value for the Genotype (-1 if non set).
     */
    public double getFrequency ( int i ) {
        if ( i>-1 && i<_genotypes.length )
            return  _genotypes[i].getFrequency();
        return -1;
    }


    /**
     * Sets the genotype frequency.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param freq the frequency.
     */
    public void setFrequency ( double freq ) {
        _genotypes[0].setFrequency ( freq );
    }

    /**
     * Returns true if Genotype isNumeric and binned.
     * Used only with Binning Algorithm.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return true or false.
     */
    public boolean isBinned() {
        return ( _genotypes[0].isNumeric() && _isBinned );
    }

    /**
     * Adds the Genotype to be binned and sets this Object as a binned Genotype.
     * Used only with Binning Algorithm.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param o the Genotype to bin.
     */
    public void add2binGenotype ( Genotype o ) {
        if ( o == null || !o.isNumeric() )
            _toBin = new Genotype ( getName() );
        else {
            _isBinned = true;
            _toBin = o;
        }
    }

    /**
      * Overriding toString method.
      * The method uses PairAllele with position 0 in the PairAllele list.
      *
      * @return a string with "marker name: allele1 - allele2"
      */
    public String toString() {
        String date = getDate();
        if ( !date.equals ( "" ) )
            date = " [" + date + "]";
        if ( _isBinned ) {
            String err = "";
            float dist=  Math.max ( getAllele ( 1 ).getDistance ( get2bin().getAllele ( 1 ) ),
                                    getAllele ( 2 ).getDistance ( get2bin().getAllele ( 2 ) )  );
            if ( dist > maxDistanceFromBinned )
                err = " *";
            return getName() + ": (src) " + _toBin.getAll1() + " - " + _toBin.getAll2()
                   + " | (bin) " + getAll1() + " - " + getAll2() + date + " " +dist + err;
        } else
            return _name + ": " + getAll1() + " - " + getAll2() + date;
    }

    /**
     * The method gets the Genotype/Marker name.
     *
     * @return the genotype name.
     */
    public String getName() {
        return _name;
    }

    /**
     * Returns the date of the Genotype.
     *
     * @return the String with date.
     */
    public String getDate() {
        return _date;
    }

    /**
     * Returns the numeric Genotype with rounded Alleles.
     * If alleles of the Genotype are float then the alleles value are rounded.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the Genotype. Returns this if not numeric.
     */
    public Genotype getIntGenotype() {
        if ( isNumeric() ) {
            return new Genotype ( getName(),
                                  _genotypes[0].getAllele ( 1 ).getIntAllele(),
                                  _genotypes[0].getAllele ( 2 ).getIntAllele() );
        }
        return this;
    }

    /**
     * Returns true if the marker has Even alleles (112, 114, 116, ...).
     * If values of alleles are float then the method uses the rounded integer.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return true or false.
     */
    public boolean isEven() {
        return ( _genotypes[0].getAllele ( 1 ).isEven() && _genotypes[0].getAllele ( 2 ).isEven() );
    }

    /**
     * Returns true if the marker has Odd alleles (111, 113, 115, ...).
     * If values of alleles are float then the method uses the rounded integer.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return true or false.
     */
    public boolean isOdd() {
        return ( !isEven() );
    }

    /**
     * Returns an array containing values of allele1 and allele2.
     * There is a check to see if the numeric flag is true.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the array of alleles.
     */
    public String[] getAlleles() {
        String pp[] = { _genotypes[0].getAll1(), _genotypes[0].getAll2() };
        return pp;
    }

    /**
     * Returns the PairAllele object in position <i>pos</i>.
     * The method returns the not valid PairAllele object if not defined.
     *
     * @param pos the position.
     * @return the PairAllele object in position <i>pos</i>.
     */
    public PairAllele getPairAllele ( int pos ) {
        if ( _genotypes.length>pos && pos>-1 )
            return _genotypes[pos];
        return new PairAllele();
    }

    /**
     * Returns the distance between two numeric PairAllele objects.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param a a PairAllele object.
     * @return the float value. Returns -1 if a PairAllele is not valid and numeric.
     */
    public float getDistance ( Genotype a ) {
        if ( isNumeric() && a.isNumeric() )
            return getPairAllele ( 0 ).getDistance ( a.getPairAllele ( 0 ) );
        return -1;
    }

    /**
      * Returns the String containing the concatenation of the two alleles.
      * If the genotype is numeric then the string is "allele1" + ":" + "allele2"
      * else the string is "allele1" + "allele2".
      * The method uses PairAllele with position 0 in the PairAllele list.
      *
      * @return the concatenation String.
      */
    public String getConcatAlleles() {
        return _genotypes[0].getConcatAlleles();
    }

    /**
     * Returns the Genotype before the binning operation (observed genotype).
     * Used only with Binning Algorithm.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return a Genotype.
     */
    public Genotype get2bin() {
        if ( isNumeric() && _isBinned )
            return _toBin;
        return ( Genotype ) this.clone();
    }

    /**
     * Returns the distance between the binned and the observed genotypes
     * (0 if the Genotype isn't binned).
     * distance=this.getDistance( get2bin() )
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the distance.
     */
    public float getDistanceFromObs() {
        return getDistance ( get2bin() );
    }

    /**
     * Returns the array containing the values of the two alleles
     * before the binning operation.
     * Used only with Binning Algorithm.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @return the values.
     */
    public String[] get2binAlleles() {
        return get2bin().getAlleles();
    }

    /**
     * Checks if the Genotype may be a son of the input Genotype objects.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param father father's Genotype.
     * @param mother mother's Genotype.
     * @return true or false.
     */
    public boolean isSon ( Genotype father, Genotype mother ) {
        Genotype fth = father;
        Genotype mth = mother;
        Allele a1 = _genotypes[0].getAllele ( 1 );
        Allele a2 = _genotypes[0].getAllele ( 2 );
        // check father and mother
        if ( fth == null )
            fth = new Genotype ( getName() );
        if ( mth == null )
            mth = new Genotype ( getName() );
        // input genotype are not correct
        if ( ! ( fth.getName().equals ( getName() ) && mth.getName().equals ( getName() ) ) )
            return false;
        return ( ( fth.isCompatible ( a1 )
                   && mth.isCompatible ( a2 ) )
                 || ( fth.isCompatible ( a2 )
                      && mth.isCompatible ( a1 ) ) );
    }

    /**
     * Returns an Allele array containing every valid allele observed
     * in the given Genotype list.     *
     *
     * @param gg Genotype array.
     * @return Allele array. null if there is no Allele.
     */
    final public static Allele[] getObservedAlleles ( Genotype[] gg  ) {
        HashSet res = new HashSet();
        if ( gg!=null )
            for ( int i=0; i<gg.length; i++ ) {
                res.add ( gg[i].getAllele ( 1 ) );
                res.add ( gg[i].getAllele ( 2 ) );
            }
        if ( res.size() > 0 ) {
            Allele rr[] = new Allele[ res.size() ];
            int i = 0;
            for ( Iterator it = res.iterator(); it.hasNext(); )
                rr[ i++ ] = ( Allele ) it.next();
            Arrays.sort ( rr );
            return rr;
        }
        return null;
    }

    /**
     * Checks if the Allele is present in the Genotype.
     * Returns true if the Genotype is not valid.
     * Returns true if the Allele is not valid;
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param allele the Allele object.
     * @return true or false.
     */
    public boolean isCompatible ( Allele allele ) {
        if ( allele.isValid() && isValid() )
            return ( allele.equals ( getAllele ( 1 ) )
                     || allele.equals ( getAllele ( 2 ) ) );
        return true;
    }

    /**
     * Overriding equals method.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param o the object to compare.
     * @return true or false.
     */
    public boolean equals ( Object o ) {
        Genotype oth = ( Genotype ) o;
        return ( getName().equals ( oth.getName() )
                 && getAllele ( 1 ).equals ( oth.getAllele ( 1 ) )
                 && getAllele ( 2 ).equals ( oth.getAllele ( 2 ) ) );
    }

    /**
     * Overriding clone method.
     *
     * @return the new cloned object.
     */
    public Object clone() {
        Genotype cloned = new Genotype ( getName(),
                                         getAllele ( 1 ), getAllele ( 2 ),
                                         getDate(), getFrequency(), getPairAllele ( 0 ).isMantainAllelesOrder() );
        if ( size() >1 )
            for ( int i=1; i<size(); i++ )
                cloned.addPairAllele ( getPairAllele ( i ) );

        return ( Object ) cloned;
    }

    /**
     * Overriding compareTo method.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param o the object to compare.
     * @return an integer less or equal or major of 0.
     */
    public int compareTo ( Object o ) {
        return _genotypes[0].compareTo ( ( ( Genotype ) o ).getPairAllele ( 0 ) );
    }

    /**
     * Returns the genotype header String of the CSV file.
     *
     * @param marker the marker name.
     * @param separator the given separator.
     * @param type the output type (TODO).
     * @return the String.
     */
    public static String getHeaderCSV ( String marker, String separator, String type ) {
        return getString4CSV ( marker + "_1" , marker + "_2", separator, type );
    }

    /**
     * Returns the genotype String of the CSV file.
     * The method uses PairAllele with position 0 in the PairAllele list.
     *
     * @param separator the separator.
     * @param nullValue value to be used to print not valid alleles.
     * @param type output type (TODO).
     * @return the String.
     */
    public String getStringCSV ( String separator, String nullValue, String type ) {
        if ( isValid() )
            return getString4CSV ( getAll1(), getAll2(), separator, type );
        else
            return getString4CSV ( nullValue, nullValue, separator, type );
    }

    final private static String getString4CSV ( String val1, String val2, String sep, String type ) {
        StringBuffer res = new StringBuffer();
        res.append ( val1 );
        res.append ( sep );
        res.append ( val2 );
        return res.toString();
    }

} // end class

